Del curso "Python y Machine Learning: de 0 a 100 con Reinforcement Learning"
Impartido por MalagaAI, [Andrés Matesanz][mate] y [Joaquín Terrasa][quim]
24 de Marzo de 2020
Duración estimada: 3 horas: 2 en streaming + 1 en trabajo individual.
Capítulo anterior (Introducción a Python): https://github.com/Matesanz/C0-intro-python
from IPython.display import YouTubeVideo
YouTubeVideo("tGsKzZtRwxw", width=100, height=100)
DISCLAIMER: ¡Eres beta-tester de este curso! 😝 Si tienes sugerencias o ves fallos, por favor, comunicanoslo 💚
Este notebook está orientado a introducirte un poco más en la programación computacional - ¡ojito! Aunque lo centramos en Python, la mayoría de conceptos son aplicables a otros lenguajes para ciencia de datos (R, Julia, C++, etc.). El contenido se agrupa en 3 bloques principales, de los cuales resaltamos dos por estar estrictamente relacionados con la ciencia de datos:
Además, al final del notebook, así como en cada bloque, hay varios recursos para que podáis ir avanzando por vuestra cuenta. Antes de comenzar, hay 3 modos de obtener este notebook.
.zip o ejecutas git clone) y usas el notebook en local. Yo uso JupyterLab pero también podeis usar IPython.
Espero que todo funcione 🤞🤞
En la anterior clase pudimos iniciarnos con Python, el lenguaje más cool del último lustro. Supiste cómo instalar Python, qué es Anaconda y para qué puede sernos útil, y empezaste a programar en Python.
En este bloque, entenderás mejor por qué Python está partiendo la pana. Verás conocimientos fundamentales para cualquier desarrollador Python, y en concreto, iremos orientándonos a ser desarrollador Python para Machine Learning.
Como quizás ya sepas, Python no solo se usa para Machine Learning. Pero... ¿y por qué se usa?

Creo que este punto es importante porque, por un lado, te hace mejor programador, al razonar los motivos por los que programas en $X$ y no solo porque mi empresa o universidad trabaja con $X$; y por otro lado, puedes lograr a aprovechar las ventajas del lenguaje que usas.
Fíjate! En Python no usamos corchetes
{}para definir funciones ni bucles. Por defecto tampoco usamos tipos y además, se evita el uso de notación camelCase (miFuncion). Además, hay guias oficiales de cómo escribir código y documentación en Python 😌
Fíjate! Al contrario que en Java o C, para guardar un número entero, no tienes que asignarle el tipo
int. Por suerte, sí está fuertemente tipado, lo que hace que no puedas, por ejemplo, sumar1 + "1", cosa que sí ocurre en JavaScript.
En concreto, Python usa algo llamado duck typing: si se parece a un pato y hace 'quack' como un pato, entonces es un pato
Fíjate! El lenguaje es de código abierto, por lo que es la comunidad la que propone cambios.
Fíjate! Python es uno de los lenguajes base de las distribuciones Linux, estando presente también en MacOS.
Por ésto y por otras cosas, es porque está en varios rankings (PYPL, TIOBE) como uno de los lenguajes más populares.
En este bloque revisamos prácticas de programación en Python (aunque aplicables a otros lenguajes), con el fin de facilitar tu manejo con Python.
Las versiones de Python vienen a ser como la familia de Cletus de Los Simpsons:

En Python, puedes usar help() sobre cualquier variable, palabra predefinida u objeto para obtener más información.
help(None)
Otro recurso útil a la hora de aprender es conocer las palabras reservadas de Python - ésto podemos revisarlo de manera sencilla, y nos servirá para aprender para qué se usa cada palabra:
import keyword
print(keyword.kwlist)
También vale la pena conocer las funciones incluidas (built-in functions) en Python, las cuales puedes usar sin tener que importar ningún paquete:
import builtins
print(dir(builtins))
Por último pero no menos importante, habrá ocasiones donde necesites más funcionalidades que las que te da Python de entrada. Una de las mejores cosas de Python es su libreria estándar, la cual contiene (en la versión 3.7) un total de $211$ módulos, entre los que podemos encontrar:
re: para tratar con expresiones regulares.datetime: para tratar con formatos de fecha y hora.socket: para conexiones de red a bajo nivel.random: para generar números aleatorios.tkinter: para crear GUIs (graphical user interface).os: para interactuar con el sistema. Contiene a su vez los módulos sys y path.pickle: para serializar objetos.Podemos formatear el texto en Python de varias maneras. Veamos cuáles hay:
print("Hoy es", 24, "de Marzo de", 2020) # sintaxis clasica
print("Hoy es %d de Marzo de %d" % (24, 2020)) # sintaxis similar a C (printf, sprintf)
print("Hoy es" + " 24 " + "de Marzo de" + " 2020") # sintaxis similar a Java (System.out.print)
print("Hoy es {} de Marzo de {}".format(24, 2020))
dia, anyo = 24, 2020
print(f"Hoy es {dia} de Marzo de {anyo}") # sintaxis similar a JS ES6 y Bash [solo disponible a partir de 3.6]
mi_dinero = 1_000_000
print(f"Tengo {mi_dinero} de € en el banco 🤑")
¿Recuerdas los tipos de datos principales que dimos en el capítulo 0?

Todos estos tipos se pueden comprobar con la función type(). Además, hay que tener en cuenta otros tipos fundamentales:
¿Conoces null? En Python no existe, pero tenemos None, cuyo tipo es NoneType.
Toda clase que creemos se considera un tipo nuevo. Por ello, toda instancia del objeto es del tipo de la clase.
Si tenemos una clase
Personay una instancia/objetopepe = Persona(edad=32, altura=180),type(pepe)esPersona
Además, en Python todas las funciones son objetos. Esto se conoce como First-Class Functions y permite, entre otras cosas, usar las variables como parámetros para las funciones, y almacenar las funciones en bases de datos.
Podemos ver todos los tipos aquí.
Muy simple: en vez de usar ==, en Python podemos usar is (la cual también podemos usar para comprobar equivalencia de datos):
lista1 = [1, 3, "5", 7, "9", None]
filtrar_errores = [x for x in lista1 if x is not None]
sumar_dos = [x + 2 for x in filtrar_errores if type(x) is int]
print(filtrar_errores)
print(sumar_dos)
# tambien podemos usar "is" para
temp_var = 10 // 2
if temp_var is 5:
print("Perfecto! No usaremos mas '=='")
# Veamos los tipos de la clase Persona
class Persona:
def __init__(self, edad, altura):
self.edad = edad
self.altura = altura
def info(self):
print(f"Mi edad es {self.edad} y mi altura es {self.altura}")
pepe = Persona(32, 180)
type(pepe)
type(pepe.info)
type(Persona.info)
import re
type(re)
type((x for x in range(10)))
type([x for x in range(10)])
type(None)
Bueno... más o menos. Realmente Python permite añadir anotaciones de tipo. ¿Qué significa esto? Pues que sin una aplicación externa (como un plug-in de un IDE) no es posible aprovechar esta opción, pues el intérprete de Python no lo comprueba. Esta aplicación externa se ejecuta antes que el intérprete, y para la ejecución si encuentra algún conflicto de tipos, como ocurre en los lenguajes estáticamente tipados.
Esta posibilidad se introduce en Python 3.5 y se mejora en las siguientes versiones. Usaremos el módulo typing para ello.
from datetime import datetime as dtime
class Persona:
duracion_anyo: int = 365
def __init__(self, nombre: str, altura: float, fecha_de_nacimiento: str):
self.nombre = nombre
self.altura = altura
self.fecha_de_nacimiento = dtime.strptime(fecha_de_nacimiento, "%Y-%m-%d")
def wave(self):
diferencia = dtime.now() - self.fecha_de_nacimiento
edad_actual = diferencia.days // Persona.duracion_anyo
print(f"Mi nombre es {self.nombre}, tengo {edad_actual} años y mido {self.altura} cms.")
# Necesitamos insertar la fecha en el formato "YYYY-MM-DD" !
Quino = Persona("Quino", 181, "1996-11-11")
Quino.wave()
Existen, además, tipos auxiliares como Union, que permiten dos tipos para una sola variable, y Optional, que permite que la variable sea de un tipo o None.
from typing import Any, Union, Optional, NewType
# Los tipos (para tipar) empiezan por mayusculas y si usan camelCase
variable_temporal: Any = 3.14 # acepta cualquier tipo. Similar a TypeScript, no?
TextoHTTP = Optional[str] # imagina que haces una peticion HTTP y no llega...¿que haces? Bien puedes capturar un error, u obtener un tipo 'None'
# Y si estas haciendo un clasificador de razas de gatos y perros, y quieres que te devuelva el tipo "Gato" o "Perro"?
TypeGato = NewType("Gato", str)
TypePerro = NewType("Perro", str)
ResultadoNN = Union[TypeGato, TypePerro]
variable_temporal = "hehe"
# A partir del 3.8 se pueden definir constantes (Final, como en JAVA)
# PI: Final[float] = 3.14
Es lo que llaman el Zen de Python. Es uno de los primeros PEP creados y sintetiza las ideas para crear codigo legible.
Lo explícito es mejor que lo implícito.
Lo plano es mejor que lo enrevesado.
Los errores nunca deberían de ser silenciados.
Ahora es mejor que nunca.
Podemos subrayar varias convenciones:
camelCasenotacion_con_barras_bajas"""De este estilo""" para cada funcion_variable_protegida__variable_privadaOtro de los PEP fundamentales se centra en la guía de estilo para Python: el PEP8 es una de las guías fundamentales para estandarizar codigo en Python.
class Illo:
def __init__(self):
self.nombre = "Kino"
self.__no_lo_sabras = True
var1 = Illo()
print(var1.nombre)
print(var1.__no_lo_sabras)
Como hemos podido ver antes, para lanzar excepciones o errores en Python usamos la palabra raise, y podemos capturar o controlar los errores con try / except
raise Exception("boomboclat")
try:
raise Exception("boomboclat")
except Exception as e:
print("No pasa nada mi gente, todo controlado")
print(e)
👉 más info 👈
La depuración de errores o debugging nos permite controlar momentáneamente el flujo del programa para detectar errores en tiempo de ejecución. De esta manera, podemos capturar y tratar errores que ni el compilador ni las herramientas externas nos permiten detectar.
breakpoint().
![]()
En la mayoría de IDEs o entornos de desarrollo, puedes marcar con un 🔴 en la barra lateral los puntos de interrupción o "breakpoints" cuando se ejecute el script
valor_externo = range(10) # nos llega desde otro script o programa en red
valor_actual = [1.5, 2.0]
breakpoint() # podemos leer y modificar datos durante la interrupcion
print(valor_actual + valor_externo) # queremos unir listas. ¿se podra?
breakpoint()
pdb, que activa la depuracion si y solo si un error ocurre. Tenemos que desactivarlo manualmente:%pdb
# podemos leer y modificar datos durante la interrupcion
valor_externo = range(10) # nos llega desde otro script o programa en red
valor_actual = [1.5, 2.0]
print(valor_actual + valor_externo) # queremos unir listas. ¿se podra?
%pdb
👉 más info 👈
Los decoradores son algo muy interesante, algo único de Python. Es algo así como las macros de Excel o Rust: permiten aplicar la funcionalidad de una clase o una función a uno o varios bloques del código, integrándose en la sintaxis del lenguaje y permitiendo una mejor lectura.
Podemos entender los decoradores como funciones de alto nivel, pues toman como argumento una función y devuelven otra función. Ésto lo veremos más adelante en el bloque funcional, pero quedate con ese concepto.
def agregar_barritas(funcion_con_texto):
"""Agrega barritas arriba y abajo de la funcion"""
def envoltorio():
"""Aplica las barritas al texto"""
print("==================")
funcion_con_texto()
print("==================")
return envoltorio
def illo_que_ase():
"""Una funcion que produce texto en la consola"""
print("Illo que ase")
con_barritas_todo_es_mejor = agregar_barritas(illo_que_ase)
illo_que_ase()
print("\n")
con_barritas_todo_es_mejor()
Lo realmente único de los decoradores es su azúcar sintáctico. ¡Puedes declarar el decorador sobre la funcion que escribe texto con una simple @!
@agregar_barritas
def illo_que_ase():
"""Una funcion que produce texto en la consola"""
print("Illo que ase")
illo_que_ase()
Los decoradores se usan mucho en paquetes de computación numérica. Por ejemplo, numba es una libreria que permite pre-compilar código Python a C/C++, y puede mejorar mucho código orientado a computación numérica. En este caso, usa decoradores @jit entre otros.
keys = ["nombre", "altura", "esJoven"]
values = ["quino", 180, True]
dict1 = dict(nombre="quino", altura=180, esJoven=True)
dict2 = {"nombre": "quino", "altura": 180, "esJoven": True} # al estilo JSON / JavaScript
dict3 = { key: value for (key, value) in zip(keys, values) }
print(dict3)
Algo fundamental para lenguajes funcionales como Haskell, son estructuras "pausadas", es decir, en las que todos sus valores no son calculados al crear la estructura. El ejemplo más claro está entre list y range:
lista = [0,1,2,3,4]
rango = range(0,5)
print("{} y {}".format(lista, rango))
Los generadores se definen sobre estructuras iterables, es decir, que se puedan recorrer. En cierto modo, cuando creas un iterador sobre una lista, obtienes un objeto Iterador sobre el que puedes usar una función next() para obtener el siguiente objeto en la estructura. Cuando la estructura llega a su fin, lanza un error StopIteration si ejecutas next() sobre el iterador ya vacío.
Hay dos formas de escribir generadores:
generador2 = (x ** 2 for x in [1,2,3])
print("{} {}".format(generador2, list(generador2)))
yield. De esta forma, podemos usarlo en cualquier tipo de estructura de datos iterable:def obtener_proximo_elemento(lista):
for indice, valor in enumerate(lista):
yield (indice, valor)
iterador = obtener_proximo_elemento([1,2,3,4,5,6])
iterador
print(f"{next(iterador)}, {next(iterador)}")
for i, v in iterador:
print("Indice {} valor {}".format(i,v))
next(iterador) # error!
Si quieres ir rápido, programa solo. Si quieres llegar lejos, programa con los demás
Tanto si es para desarrollar herramientas más complejas por ti mismo como si estás participando en un proyecto con otros desarrolladores, documentar adecuadamente es esencial. Python tiene su propia guía de estilo para la documentación, basada en reStructuredText y condensada en el PEP287 y PEP257. En Python, los llamamos docstrings.
De manera simple, reStructuredText es un lenguaje de texto enriquecido que compila a HTML. Veámoslo:
def saludar(nombre=None, entusiasmadamente=False):
"""Saluda a una persona, con fuerza o no.
Parametros
----------
nombre : str, obligatorio
El nombre de la persona a saludar
entusiasmadamente: bool, opcional
Elige si saludar con fuerza o no
Lanza
------
TypeError
Si el nombre es None
"""
if nombre is None:
raise TypeError("El nombre es None")
exclamacion = ["", "!"][entusiasmadamente] # equivalente a un operador ternario ?: (no recomendable! haz un 'if-else'. Esto no es tan legible)
print("Hola Don Pepito! Hola Don {}{}".format(nombre, exclamacion))
saludar("Jose", True)
saludar("Jose", False)
Fíjate! Una de las mejores maneras de documentar código en Python es asignar correctamente los nombres a funciones y variables temporales. Ten en cuenta el estilo de Python:
estoNoEstaFino,esto_si_es_mejor
👉 más info 👈
Git es un software de control de versiones que permite mantener nuestro proyecto de forma sencilla. Se centra en una serie de comandos para darle una estructura en forma de árbol a nuestro proyecto, donde podemos guardar el progreso que hacemos, asi como volver hacia atrás o abrir nuevas ramas de desarrollo (para por ejemplo nuevas características).
En Machine Learning podemos aplicarlo para, por ejemplo, aplicar varias configuraciones a los hiperparámetros de nuestro modelo, sin que haya conflictos.

👉 más info 👈
Este tema es algo más extenso, así que te daré una idea general. Realmente es algo que se hace a nivel de aplicación, cuando ya estás programando en un IDE (entorno de desarrollo), ya que en Jupyter no es del todo útil.
La idea fundamental consiste en probar todos los casos posibles de entrada y salida para tu programa, con el fin de comprobar si tu programa tiende a producir errores. Además, existe la posibilidad de simular el funcionamiento de tu programa incluso antes de haberlo programado, lo que nos ayuda a centrarnos en módulos específicos que necesitan más atención y cuidado.
Unit Testing: Se centra en comprobar que cada bloque (funciones, clases, etc.) funcionan como está previsto. Python incluye la libreria estándar unittest, así como la palabra reservada assert.
Test-Driven Development (TDD): es una metodología de programación que se centra en realizar primero los tests, para asegurar cómo debe funcionar el programa, para acto seguido implementarlo.
Mocking (Simulacion): permite simular módulos de tu app que aún no has desarrollado o sobre los que no tienes control. Un paquete bastante usado es mock.
Cobertura: facilita el comprobar si todos los bloques de código tiene pruebas de código.
La programación funcional es un paradigma de la programación, basada en el lambda calculus. Realmente ya has dado algo de programación funcional en Python, aunque no te hayas dado cuenta. Las listas por comprensión ([x for x in range(10)]) y la compilación perezosa de funciones es algo ímplicito en el lenguaje. Además, podemos ver otras características de lenguajes funcionales en Python.
Como antes comentamos, las funciones son ciudadanos de primera clase en Python. Ésto también se conoce como funciones de alto orden: virtualmente, toda función puede ser usada como parámetro de otra función o devolver otra función. Este último concepto, el de devolver otra función, también se conoce como funciones parciales, porque se aplican parcialmente.
Muchas de las utilidades funcionales están ya incluidas (built-in), en la libreria estándar functools o en itertools (para generar, por ejemplo, secuencias infinitas).
datos_json = {"nombre": "Jorge", "edad": 24, "altura": 180, "profesion": "marketing"}
datos_json["nombre"] #ok! pero y si...
datos_json["Nombre"]
datos_json.get("Nombre", "te has equivocao de clave :P")

Si has usado JavaScript, Haskell, Java 8+, ... conocerás las funciones anónimas. Éstas permiten definir una función de un solo uso en una sola línea. Digamos que las funciones que usamos, por ejemplo, para el map (mas1(valor)) solo la usamos ahí. ¿Por qué definir una función nueva, si simplemente puedes hacer...
list1 = [1,2,3]
# def mas1(valor):
# return valor + 1
list1_mas1 = map(lambda valor: valor + 1, list1)
list(list1_mas1)
👉 más info 👈
![]() |
![]() |
El lenguaje funcional permite una abstracción a la hora de modificar estructuras de datos. En vez de pensar cómo cambiar una estructura, como hacemos con un bucle for, pasamos a pensar qué queremos cambiar. Esto nos ahorra, por ejemplo, errores de acceso a memoria. Además, no tenemos que preocuparnos por iterar sobre la estructura (no usaremos más for, while, ... )
Vamos a dar 4 funciones fundamentales de los lenguajes funcionales:
map permite aplicar a cada elemento de una estructura, una función. Por ejemplo, podemos sumar 1 a todos los elementos de una lista:list1 = [1,2,3]
def mas1(valor):
return valor + 1
list(map(mas1, list1))
filter permite seleccionar los elementos de una estructura que cumplan la función usada. Por ejemplo, podemos escoger solo los numeros pares de una lista:def esPar(valor):
return valor % 2 == 0
list(filter(esPar, list1))
reduce permite reorganizar una estructura A para obtener otra estructura B. Normalmente, se usa para condensar estructuras complejas y conseguir estructuras mas simples, con valores acumulados. Por ejemplo, podemos sumar todos los elementos de una lista para obtener un valor total (una estructura más simple):from functools import reduce
def sumar(valor1, valor2):
# ojito! Ten en cuenta que, para 'reduce' el valor se acumula en el segundo (2do) valor
return valor1 + valor2
reduce(sumar, list1)
zip permite agrupar varias estructuras $A_x$ en una sola estructura $B$. La estructura $B$ está formada por tuplas de los elementos de las estructuras $A_x$. Por ejemplo, podemos agrupar los nombres y alturas de personas en una sola lista:nombres = ["Quino", "Andres", "Fernando"]
altura = [181, 178, 180]
list(zip(nombres, altura))
¿Por qué ocurre ésto? ¿Por qué hay que usar list? Respuesta: generadores (mira más abajo) 👇
print(map(mas1, lista1))
print(filter(esPar, lista1))
print(zip(nombres, altura))
Los llamados lenguajes funcionales puros es un sub-paradigma que se centra en que el resultado de cualquier función dependa únicamente en los parámetros que le pasas, es decir, si ejecutas 2 veces una función con los mismos parámetros, ambas ejecuciones te devolverán lo mismo. Por ello, si usamos variables mutables, puede pasar esto:
from random import randint
def agrega_numero(lista):
lista.append(randint(0, 1e4))
return lista
x = list()
agrega_numero(x) # ¿Que devuelve?
agrega_numero(x) # ¿Que devuelve?
👆 ahí vemos que Python no es puramente funcional (ni queremos 😛).
Si ya conoces algun lenguaje con variables inmutables, sabrás que (en general) tienen mejor rendimiento que los lenguajes que no: Clojure, Haskell, Scala, Rust... Las ventajas de las estructuras inmutables, es que proporcionan paralelismo de datos, mayor seguridad y código más simple.
Algunas estructuras inmutables en Python son las tuplas, los sets "congelados" (*frozenset*), `range` y los tipos básicos (int, float, ...)
v1 = (1, True)
v1[0] = 3
values = ("tomeu", 180, 25, "ingles c1")
my_var = frozenset(values)
my_var
👉 más info 👈
👉 ¿algo práctico? 👈
Quizás ya te hayas preguntado por qué Python compila código que no es correcto, siempre que esté dentro de funciones o clases. Ésto es porque Python usa evaluación perezosa dentro de funciones. Esta funcionalidad no solo está en la evaluación de funciones, sino también en algo llamado generadores.
Básicamente, Python permite compilar una función cuando es declarada y solo comprobar si está correctamente definida cuando se ejecuta al menos una vez.
👉 más info 👈
def esto_no_hace_nada(illo):
asdlkjqwelkj
esto_no_hace_nada(1)
generador_1 = range(10)
generador_2 = enumerate([10,20,30])
generador_3 = reversed([10,20,30])
print(generador_1, type(generador_1))
print(generador_2, type(generador_2), list(generador_2))
print(generador_3, type(generador_3), list(generador_3))
La idea principal es que toda función $A$ con $N$ parámetros devuelve otra función $B$ con $N-1$ parámetros, donde $B$ tiene un estado interno diferente a $A$. Ésto quiere decir que, al igual que las clases, las funciones pueden mantener un estado interno (como la referencia self en las clases!). Un ejemplo clásico es el cálculo del número n de fibonacci:
$f_{0}=0\,$
$f_{1}=1\,$
$f_n = f_{n-1} + f_{n-2}$
def configurar_fib():
a, b = 0, 1 # mantenemos un estado interno en la función. Esto se conoce como "memoization"
def fib(n):
if n == 0:
return a
elif n == 1:
return b
else:
return fib(n-1) + fib(n-2)
return fib
fib = configurar_fib()
[fib(x) for x in range(13)]
Existen dos maneras de crear funciones parciales en Python. La primera es hacerlo explícitamente:
def sumar_cantidad(cantidad):
def sumar(otro_elemento):
return otro_elemento + cantidad
return sumar
sumar_dos = sumar_cantidad(2) # creas una funcion aplicada parcialmente
print(f"2 + 4 = {sumar_dos(4)} | 10 + 2 = {sumar_dos(10)}")
Otra manera es usar el método partial:
from functools import partial
def sumar(a, b):
return a + b
sumar_dos = partial(sumar, b=2) # hay que nombrar los parametros que vamos a "sobrescribir"
print(f"2 + 4 = {sumar_dos(4)} | 10 + 2 = {sumar_dos(10)}")
Existen, además, conceptos relacionados, como la currificación o las clausuras.
Python es muy popular para Machine Learning. Algunas de las librerias más conocidas son numpy, pandas, matplotlib, scikit-learn, scipy, y algunas orientadas a redes neuronales, como son tensorflow o pytorch.
En este notebook veremos dos paquetes esenciales: numpy y pandas.
Numpy es el paquete de computación numérica más usado de Python. Proporciona una infinidad de estructuras de datos y funciones que permiten el manejo masivo de datos. Es rápido gracias a que ejecuta C por debajo (como si no 😉), y permite realizar operaciones entre conjuntos grandes de datos, con un estilo similar a lo que se puede hacer en Matlab y R.
Veamos un ejemplo:
lista1 = [1,2,3]
lista2 = [0,0,0]
lista1 + lista2
No era lo esperado, ¿no?
En numpy, la estructura básica es el array, que es el "equivalente" a la lista en Python. Al igual que una lista, puede tener 1 o más dimensiones y puede tener varios tipos de datos alojados, aunque lo más frecuente es que solo tenga un tipo.
Aún así, la estructura más común es el nd-array o array multidimensional, pues permite almacenar datos en varios niveles de organización. De hecho, los arrays multidimensionales son inmutables.
import numpy as np
lista1 = np.array([1,2,3])
lista2 = np.array([0,0,0])
lista1 + lista2
La mayoría de funciones para crear estructuras de datos, necesitan conocer la forma de la matriz que queremos generar. Entendemos que una lista de N valores es una matriz $1 \times N$ o bien $N \times 1$
matriz_cuadrada = (4,4) # siempre se usan tuplas!
ceros = np.zeros(matriz_cuadrada)
matriz_identidad = np.identity(4) # las matrices identidades siempre son cuadradas
print(matriz_identidad + ceros)
np.identity(4)
np.empty((3, 3))
np.ones((2,2)) # fijate en los parentesis de la tupla!
# np.ones(2,2) # da error!
arr0 = np.array([0.2, 0.4, 0.6])
print(f"arr0.shape = {arr0.shape}") # por que??
arr0.shape = (3, 1)
print("\n", arr0)
print("\n", arr0.transpose())
arr1 = np.array([1,2,3])
arr2 = np.array([4,5,6])
print(f"[arr1; arr2] = \n{np.vstack([arr1, arr2])}")
arr3 = np.array([1,2,3])
arr3.shape = (3, 1)
arr4 = arr3 + .5
print(f"[arr3 arr4] = \n{np.hstack([arr3, arr4])}")
np.mean(arr1)
print(f"sum({list1}) = {sum(list1)}")
print(f"np.sum({arr1.transpose()}) = {np.sum(arr1)}")
Una característica fundamental de numpy es el broadcasting o "propagación", que asegura la posibilidad de realizar operaciones aritmeticas (suma o multiplicacion) entre matrices que, bien no tienen las mismas dimensiones, pero son compatibles. Veámoslo:

Si quieres saber más de numpy, te recomiendo el siguiente enlace:
👉 más info 👈
pandas es la libreria estrella cuando hablamos de ciencia de datos en Python. En cierto modo, trata de traer a Python las tablas de R o data.frames (df pa' los amigos), algo así como una matriz con esteroides.
pandas permite leer y modificar conjuntos de datos (datasets) de manera sencilla, usando una sintaxis similar a numpy para el manejo de matrices. Además, proporciona utilidades para el manejo de ficheros, asi como para detectar datos "vacíos" y calcular valores acumulados.

import pandas as pd
import seaborn as sns # ejecuta la cápsula de abajo y vuelve a ejecutar ésto!
# si escribes "!" al principio de la linea, puedes llamar a programas y comandos del shell / linea de comandos (ya sea shell, bash, cmd o Powershell)
!pip install seaborn
import pandas as pd
import seaborn as sns
# otra manera es cargarlo directamente
# iris = pd.read_csv('https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv')
iris = sns.load_dataset('iris')
iris.head()
print(f" Tipo: {type(iris)}\n Columnas: {list(iris.columns)}\n")
iris[1:4] # seleccionar una o mas filas [inicio, final)
iris["species"] # seleccionar columna
iris.loc[1:4, "species"] # seleccionar un subconjunto del dataframe [inicio, final]
iris.iloc[1:4, 4] # seleccionar un subconjunto del dataframe, escogiendo la columna por su posicion [inicio, final)
Fíjate! Si al final del comentario ves [inicio, final), es porque el ultimo indice no se extrae (ej. 1:4 solo extrae las filas 1,2,3)
iris.shape
iris[iris["species"] == "virginica"].head(3)
iris.dropna().shape # por suerte este dataset nos lo dan limpito :)
iris.groupby("species").mean()
iris["sepia_Length"] = iris["sepal_length"] / iris["sepal_width"]
iris.head(3)
Fijate que pandas aplica conceptos funcionales, como la inmutabilidad de datos al modificar dataframes 👇
iris.rename(columns = { "sepia_Length": "borra_esta" }, inplace=True) # ojito! Algunas funciones devuelven una copia del df modificado
iris.head(3)
Ten en cuenta que, al igual que con el manejo de BBDD, una función poco eficiente para filtrar varias tablas puede significar 2h más esperando.
De cara al tratamiento de datos, hay una forma bastante popular en la comunidad de R llamada tidy data ("datos organizados"). Esta metodología se centra en usar un cjto. de verbos para referirnos a la modificación de datos (algo parecido a prog. funcional).
👉 más info para aprender más de tidy data 👈
👉 más info para aprender más de pandas 👈
Normalmente en Python se usa matplotlib, una libreria para visualizacion de datos cientificos. Aun asi, yo no soy muy fan, pues es muy similar a la forma de dibujar graficos de MatLab 🙂 Aquí hay algunas alternativas:
ggplot2. En Python tienes plotnine y ggpy, que usan casi la misma API.ggplot2 en R.Vamos a ver cómo se usan matplotlib y plotly en Python. Para ello, usaremos el dataset iris o bien datos aleatorios.
Es, con diferencia, la libreria más usada para DataViz en Python
%matplotlib inline # realmente solo es necesario hacerlo una vez (al menos, en JupyterLab)
from matplotlib import pyplot as plt
from numpy.random import randn
ts = pd.Series(randn(1000), index=pd.date_range('1/1/2019', periods=1000))
ts = ts.cumsum()
ts.plot()
plt.figure()
ts.plot(style='k--', label='Series')
plt.legend()
df = pd.DataFrame(np.random.rand(10, 4), columns=['a', 'b', 'c', 'd'])
df.plot(kind='bar');
df.plot(kind='bar', stacked=True)
iris.head(1)
%matplotlib inline
plt.figure()
plt.hist(iris["sepal_length"], bins=15)
plt.xlabel("Longitud del sépalo")
plt.ylabel("Individuos")
plt.show()
matplotlib facilita además visualizaciones complejas, como la de *Coordenadas Paralelas*, que permite visualizar datos multidimensionales, permitiendo ver los clústers de datos (en este caso, respecto a una variable categórica *'species'*) a lo largo de varias variables cuantitativas.
from pandas.plotting import parallel_coordinates
plt.figure()
parallel_coordinates(iris, 'species', colormap='winter')
plt.show()
👉 más info para aprender más de matplotlib 👈
Si quieres realizar visualizaciones interactivas, plotly es una de las librerias más usadas. Permite infinidad de configuraciones, y es extensible por definición, aunque usando JavaScript. Asimismo, tiene equivalentes en R y JavaScript. Plotly además incluye Plotly Express: An easy-to-use, high-level interface to Plotly, which operates on "tidy" data and produces easy-to-style figures.
Una de sus desventajas, al igual que pasa con otras librerias de visualizacion interactivas, es que no cuentan con gran soporte dentro de Jupyter o similares, por lo que siempre tienes que saber algunos trucos - de hecho, en un primer momento, matplotlib tampoco tenia un buen soporte y era confuso obtener la salida dentro de Jupyter.
# !pip install plotly "ipywidgets==7.5" # valido para jupyter-notebooks
# para jupyter-lab es recomendable mirar la documentacion de Plotly, pues es mas enrevesado
import plotly.io as pio
pio.renderers
pio.renderers.default = "jupyterlab"
import plotly.graph_objects as go
fig = go.Figure(
data=[go.Bar(y=[2, 1, 3])],
layout_title_text="A Figure Displayed with fig.show()"
)
fig.show(renderer="browser")
import plotly.graph_objects as go
fig = go.Figure(
data=[go.Bar(y=[2, 1, 3])],
layout_title_text="A Figure Displayed with fig.show()"
)
# necesita las siguientes librerias para: jpg/svg/png
# !pip install requests psutil
# conda install -c plotly plotly-orca -y
fig.show(renderer="jpg")
import plotly.graph_objects as go
fig = go.Figure(
data=[go.Bar(y=[2, 1, 3])],
layout_title_text="A Figure Displayed with fig.show()"
)
fig.show(renderer="jupyterlab")
import plotly.graph_objects as go
import numpy as np
x = np.arange(10)
fig = go.Figure(data=go.Scatter(x=x, y=x**2))
fig.show()
import plotly.express as px
df = px.data.iris()
fig = px.scatter(df, x="sepal_width", y="sepal_length", color="species")
fig.show()
import plotly.express as px
df = px.data.gapminder().query("year == 2007")
fig = px.line_geo(df, locations="iso_alpha",
color="continent", # "continent" is one of the columns of gapminder
projection="orthographic")
fig.show()
import plotly.express as px
df = px.data.iris()
fig = px.scatter(df, x="sepal_width", y="sepal_length", color="species", marginal_y="rug", marginal_x="histogram")
fig
import plotly.express as px
df = px.data.gapminder()
fig = px.scatter(df, x="gdpPercap", y="lifeExp", animation_frame="year", animation_group="country",
size="pop", color="continent", hover_name="country", facet_col="continent",
log_x=True, size_max=45, range_x=[100,100000], range_y=[25,90])
fig.show()
Una de las formas más sencillas y prácticas de aprender y entrenar tu habilidad con Python en el análisis de datos es aprovechar el open data o la iniciativa de datos abiertos. Desde datos recolectados por empresas u organizaciones, hasta datos generados continuamente por los organismos públicos:
import pandas as pd
# info: https://datosabiertos.malaga.eu/dataset/padron-de-habitantes-por-distrito-y-seccion-censal-2019/resource/24ff297b-e7de-4c35-9477-3f45917bb4b3
padron_malaga_2019 = pd.read_csv("https://datosabiertos.malaga.eu/recursos/demografia/padron/2019/padrondistritosysecciones.csv") # si tarda en cargar, agregar ', chunksize=10_000'
padron_malaga_2019.head()
print(padron_malaga_2019.dtypes, "\n\n", padron_malaga_2019.columns)
codigos_municipales = pd.read_csv("http://datosabiertos.malaga.eu/recursos/demografia/padron/tablas-catalogo/municipio.csv")
codigos_municipales.head()
codigos_municipales[codigos_municipales["MUNICIPO"] == "Málaga"]
print("ojito! Pandas usa el 'AND a nivel de bit' (bitwise) en vez del 'AND logico'")
print("AND logico: and, AND a nivel de bit: &\n\n")
# usamos un filtro booleano (muy eficiente)
filtro = (padron_malaga_2019["CMUNN"] == 67) & (padron_malaga_2019["SEXO"] == 6) & (padron_malaga_2019["EDAD"] > 40)
ciudadanas_femeninas_mayor40_malaga = padron_malaga_2019[filtro]
print("Total:", len(ciudadanas_femeninas_mayor40_malaga))
ciudadanas_femeninas_mayor40_malaga.head(3)
Creo que el contenido del capítulo ya es suficientemente denso como para seguir avanzando, así que dejo unos cuantos recursos por si quereis seguir avanzando:
Expresiones Regulares. Si no sabes lo que son, Mozilla tiene un artículo muy bueno (practica con RegExr, es genial!). En Python funcionan de otro modo. Para más avanzados, aquí.
Descubre la 🧙 magia 🧙 dentro de Jupyter. Explora sus magics, como %time, %timeit o %matplotlib aquí.
Fíjate! También son decoradores 🤩
Cómo funciona la Lectura/Escritura de archivos. Más aquí.
Este artículo repasa algunos conceptos ya vistos de manera más profunda.
Para los usuarios de Windows, recomiendo mirar el WSL o Windows Subsystem for Linux, el cual permite ejecutar sistemas Linux como Ubuntu o Debian dentro de Windows (al más puro estilo Docker).
Ve investigando alguna de las librerias que permiten aplicar modelos de Reinforcement Learning, como OpenAI gym, IntelAI coach, Unity ML Agents o PARL.
También puedes revisar librerias que te facilitan la creación y mejora de modelos, como Neptune y Tensorboard.
¿Un consejo? Busca código donde se use lo que menciono; lo más útil es que puedas ver cuándo y por qué se usan las distintas features del lenguaje - y por qué no usarlas si no es necesario.

Codewars permite practicar tus conocimientos de programación entorno a varios problemas - todo problema puedes resolverlo en más de 20 lenguajes: Python, Haskell, Java, ...
Otras plataformas como Exercism o Leetcode se centran en tracks o cursos completos, que tratan temas como Algoritmia, problemas de decisión o búsqueda.
Incluso Google ofrece un curso de Python. De cara a los próximos capítulos, recomiendo este libro sobre Python y Data Science, totalmente gratuito de manera online.
De todas formas, ésto solo es un pequeño grano en el mar de recursos que hay. Te recomiendo que hagas una búsqueda en GitHub, o pruebes en PyPi.
La cantidad de versiones: si acabas de llegar a Python y te encuentras con alrededor de 8 versiones, tu cabeza hace 🤯🤯 incluso peor, es cuando tienes que usar Python2 y Python3 a la vez, pues tienes que operar como si fuesen dos lenguajes distintos. Ésto también se acentua cuando trabajas por proyecto (menos mal que existe Anaconda y PyEnv 🥳)
La dificultad para hacer concurrencia (ésto es algo avanzado): Python tiene varias implementaciones, y CPython, la más usada, implementa una estructura, llamada GIL, que obliga a correr Python en un solo procesador.
Las implementaciones completamente hechas en Python tienden a ser lentas, comparadas con C / C++ o Rust.
Puedes encontrarnos por